<?
/**
 * CmsKVC implements a simple WOA/EOF type kvc scheme..
 *
 */

	include_once("cmsConfig.php");
	include_once(CMS_HOME_DIR . "/lib/CmsWriter.php");

	// PEAR Log
	include_once( "Log.php" );
	include_once( "Log/file.php" );	

	define ("VALIDATION_UNDEFINED_ERROR","undefined validation error");
	define ("VALIDATION_NOT_NULL","is a required value");
	define ("VALIDATION_INVALID_TYPE","is an invalid type");
	define ("VALIDATION_INVALID_REGION","the selected region is invalid");
	define ("VALIDATION_INVALID_EMAIL_ADDRESS","is not a valid email address");
	define ("VALIDATION_EMAIL_ADDRESSES_DONT_MATCH","does not match");
	define ("VALIDATION_MORE_THAN_ONE","there must be at least one");
	define ("VALIDATION_INVALID_CHILDREN","contains invalid children");

	class CmsKVC {


		var $dict;
		var $id;
		var $keys;
		var $requiredKeys;
		var $documentTypeCode;// doucmenttype used for serialising to db
		var $documentCode;// document code use serialse to/from db..
		var $dataModelKeyMapping;//
		var $formId;
		var $dataTable;
		var $cmskvcidfield;
		var $validationMessageDict;
		var $actions;
		var $allowedMimeTypes;

		//var $;//
		
		function CmsKVC(){
			
			$this->log("construct " . get_class($this));//why to the error log and not the php log???
			
			$this->id = substr(md5(uniqid(rand(), true)),rand(0,21),8);//trufle shuffle..
			$this->dict = array();
			$this->keys = array();
			$this->requiredKeys = array();
			$this->documentTypeCode = null;
			$this->formId = null;
			$this->dataModelKeyMapping = array(
				"documentCode" => "documentcode",
				"id" => "categorycode",
				"documentTypeCode" => "documenttypecode",
				"formId" => "formid"
			);//this must be a 1-1 mapping of keys and db fields.
			$this->dataTable = "content";
			$this->cmskvcidfield ="categorycode";
			$this->validationMessageDict = array();
			$this->actions = array();
			$this->allowedMimeTypes = array("*/*");

			//get the cmskvc super global thing..
			registerSessionPseudoSuperGlobal('_CMSKVC',$this->id,$this);
		}

		function id(){
			return $this->id;
		}

		//checks that the receiver is valid for saving to a datastore
		function validateForSave() {
			$this->dict['invalidKeys'] = null;
			$invalidKeys = array();
			//next loop over the keys in the datamodel and attempt to validate
			//foreach ( $this->dataModelKeyMapping as $key => $field) {
			$keysToValidate = array();
			foreach ($this->keys as $keyType) {
				$keysToValidate = array_merge($keysToValidate, $keyType);
			}
			foreach ($keysToValidate as $key){
				$res =  $this->validateKey( $key );
				if (!($res === true)){
					//here we want an explict test on true.. every other type is false.
					if(!is_string($res)) {
						$res = VALIDATION_UNDEFINED_ERROR;
					}
					//$this->markKeyAsInvalidWithMessage($key, $res);
					$invalidKeys[ $key ] = $res;
				}
			}

			$this->dict['invalidKeys'] = $invalidKeys;
			//echo "invalid keys<br/>";
			//myPrintR($this->dict['invalidKeys']);
			//next we check for validation errors
			if (count($invalidKeys)>0) {
				return false;
			} else {
				return true;
			}

		}

		// this validates the specified key on the object
		// it will return true if okay.. otherwise an error string
		// it is fail fast and will ony return the first validation error message
		function validateKey($key) {
			//first we need the value

			$value = $this->valueForKey($key);
			
			if (!is_null($value) && is_string($value) )
				$value = trim($value);//do we really need to trim on validation???
				
			//is this key a required value? is it nullable?
			
				if (in_array($key, $this->requiredKeys) && (is_null($value) 
				|| (is_array($value) && count($value) <=0 )) ) {
				return VALIDATION_NOT_NULL;
			}

			//next perform simple type check.
			$isAFunction = null;
			foreach( $this->valueForKey('keys') as $keyType => $keysOfType) {
				if (in_array($key, $keysOfType)){
					switch ($keyType){
						case EXTRACT_STRING:
							$isAFunction = "is_string";
						break;
						case EXTRACT_NUMBER:
							$isAFunction = "is_numeric";
						break;
						case EXTRACT_ARRAY:
							$isAFunction = "is_array";
						break;
						case EXTRACT_BOOLEAN:
							$isAFunction = "is_bool";
						break;
						case EXTRACT_FILE_UPLOAD:
							//need to write
							//$isAFunction = "is_file_upload";
						break;
						case EXTRACT_DATE:
							$isAFunction = "is_date";
						break;
					}
					break;// found a matching function. .can exit. the loop
				}
			}
			//this type check will only work.. if the value is non null..

			if ( !is_null($value) && !is_null($isAFunction)) {
				if (!$isAFunction($value))
					return VALIDATION_INVALID_TYPE;
			}

			//finally if we get this far ... perform custom checking.. the custom methods will be responsible for
			//deciding what to do wil nulls..
			$validateFunction = "validate"  . ucfirst( $key );
			if (method_exists($this, $validateFunction)) {
				$val = $this->{$validateFunction}();
				if (!is_null($val)){
					return $val;
				}
			}
			return true;
		}


		// determines if we have data set or not..
		function isEmpty(){
			return (count($this->dict) <= 0 );
		}

		//the receiver will take the value specified and
		// attempt to store it in the associated key
		// first we search for a method $this->set<Key>( $value )
		// then an iVar key
		// and finally, if none of the above match.
		// we bung the value in the internal dictionary.
		function takeValueForKey($value, $key) {
			$setKey =  'set' . ucfirst($key);
			if ( method_exists($this, $setKey) ) {
				$this->{$setKey}( $value );
			} else if (array_key_exists($key,$this)) {
				$this->{$key} = $value;
			} else {
				$this->dict[$key] = $value;
			}
		}
		// the receiver returns the value of the provided key.
		// first we check for a method $this-><key>
		// then for a method $this->get<Key>()
		// next we look for an ivar key
		// and finally we check for a key in the dictionary.
		function &valueForKey($key){
			if (!is_string($key)) {
				dieWithInternalServerError("key passed to valueForKey not a string");
			}
			$NULL= null;
			$getKey =  'get' . ucfirst($key);
			if (method_exists($this, $key)) {
				$r =  $this->{ $key }();
				return $r;
			} else if (method_exists($this, $getKey)) {
				$r = $this->{ $getKey }();
				return $r;
			} else if (array_key_exists($key, $this)) {
				return $this->{ $key };
			} else 	if (array_key_exists($key,$this->dict)) {
				return $this->dict[$key];
			} else {
				return $NULL;
			}
		}

		// the receiver will check for variables in the request
		// that are to be assigned by it.
		function takeValuesFromRequest(){
			//echo "take values from request: " . $this->id() . " " . get_class($this) ."<br/>";
			//we simply loop through an array that is defined.. in the super class.. and call
			// takeValuesFromArrayOfType
			$this->takeValuesFromArray($_REQUEST);
			return;
		}

		function takeValuesFromArray($array) {
			foreach ($this->keys as $keyType => $keys){
				if (is_array($keys)) {
					$this->takeValuesFromArrayOfType($keys, $array, $keyType, null);
				}
			}
		}
		//this does the heavy lifing for takeValuesFromRequest
		// it determines the the function to use for variable extraction
		// and checks for varibles int he request of <id>_<key>
		function takeValuesFromArrayOfType($valuesToTake, $array, $type, $default = null){
			$extractFunction = null;
			switch ($type) {
				case EXTRACT_STRING:
					$extractFunction = EXTRACT_STRING_FUNCTION;
				break;
				case EXTRACT_NUMBER:
					$extractFunction = EXTRACT_NUMBER_FUNCTION;
				break;
				case EXTRACT_ARRAY:
					$extractFunction = EXTRACT_ARRAY_FUNCTION;
				break;
				case EXTRACT_BOOLEAN:
					$extractFunction = EXTRACT_BOOLEAN_FUNCTION;
					if (is_null($default)) {
						$default = false;
					}
				break;
				case EXTRACT_DATE:
					$extractFunction = EXTRACT_DATE_FUNCTION;
				break;
				case EXTRACT_FILE_UPLOAD:
						//file uploads need to be handled differently.. as files are palced
						foreach ( $valuesToTake as $key) {

							$this->takeBinaryDataFromFiles($key, $this->valueForKey('allowedMimeTypes'));
						}
						return;
				break;
			}

			if (is_null($extractFunction)) return;

			//run through and extract.. the values
			foreach ( $valuesToTake as $key) {
				$rKey = $this->keyForField($key);
				//we only really want to extract if the key is in the request??
				if (key_exists($rKey,$_REQUEST)){
					$value = $extractFunction($rKey, $array, $default);//this does not perform checking..
				
				// this .. means that we are only setting things.. if we have a value.. but if 
				// we delete then we are in the shit.. see what happens now..
					$this->takeValueForKey($value, $key);

				}
			}

		}

		function invokeActions(){
			$this->invokeActionsFromArray($_REQUEST,null);
		}

		/** this method checks the incoming request and invokes the correct action.. */
		function invokeActionsFromArray($array,$default= null) {
			// we check for my actions..
			// then child component actions..
			// all actions are preceded by the class id.. and then Action .. ie <id><name>Action
			// there is a problem.. of determining what actions, however are allowed to be invoked
			// in any given state..
			$this->dict['invocationResults'] = array();
			
			foreach($this->actions as $action) {
				$actionKey = $this->keyForField($action);
				$actionName = $action . "Action";
				$actionPattern = '('.$this->id().')_('. $action .')_([0-9]+)';
				$invocations = array();
				foreach ($array as $key => $value){

					if (preg_match('/^'.$actionPattern.'$/',$key,$keyParts) && method_exists($this,$actionName )) {
						$invocations[ $keyParts[3] ] = $keyParts;
					}
				}

				ksort($invocations);
				//now we do the invocations ...
				$invocationArguments = array();
				foreach ($invocations  as $invocation) {
					//now we need to find the arguments
					foreach ($array as $key=> $value) {
						$actionArgumentPattern = $invocation[0] . '_([0-9]+)';
						if (preg_match('/^'.$actionArgumentPattern.'$/',$key,$keyParts)) {
							$invocationArgumentValue = extractValueWithKeyFromArray($keyParts[0],$array,$default);
							if (!is_null($invocationArgumentValue)){
								$invocationArguments[ $keyParts[1] ] = $invocationArgumentValue;
							}
						}
					}
					//invoke it
					ksort($invocationArguments);
					//store the invocation results
					$this->dict['invocationResults'][$action] = call_user_func_array(array(&$this,$actionName) , $invocationArguments);
				}
			}
		}

		//
		function takeBinaryDataFromFiles($key, $allowedMimeTypes) {
			//echo "foof";
			// check for uploaded file data
			// and move/store in session/tempcache.. and remember
			// mime data etc.
			$fKey = $this->keyForField( $key );

			if (key_exists($fKey, $_FILES)){
				//import the image and store.. only if in list of allowed mime types

				if (key_exists('tmp_name', $_FILES[$fKey]) &&is_uploaded_file($_FILES[$fKey]['tmp_name']) &&
					$_FILES[$fKey]['error'] == 0
					&&
					isMimeTypeAllowed($this->allowedMimeTypes,$_FILES[$fKey]['type'] )
					){//in_array( $_FILES[$fKey]['type'], $allowedMimeTypes)){
					//suck the thing into memory.. and bung it in a session???

					$uploadDir = CMS_KVC_UPLOAD_TEMP_IMAGES . "/" . $this->valueForKey('id');


					if (!file_exists($uploadDir)) {
						//make the directory
						mkdir( $uploadDir );
					}

					$uploadFileName = $uploadDir. "/" . $this->valueForKey('id') . "." . imageFileExtensionForMimeType( $_FILES[$fKey]['type']);
					if ( move_uploaded_file($_FILES[$fKey]['tmp_name'], $uploadFileName)){
						$_FILES[$fKey]['file'] =$uploadFileName;
						unset($_FILES[$fKey]['tmp_name']);
						$this->takeValueForKey($_FILES[$fKey], $key );
						//unset($_FILES[$fKey]);
					}
				}
			}
		}

		//this returns a key that this object will pick up for a given field/value
		function keyForField($field){
			return $this->valueForKey('id') . "_" . $field;
		}

		function isKeyValid($key){
			$ik = $this->valueForKey('invalidKeys');
			if (is_array($ik)){
				return !key_exists($key, $ik);
			}
			return true;
		}


		function save(&$authInfo, &$db ){

			///now.. do the mapping.. we do no validation here.. as we assume that its all valid?
			$dataToWrite = array();
			foreach ($this->dataModelKeyMapping as $key => $field) {
				$v = $this->valueForKey($key);
				if (true) {
					//check for binary data.. or if the key is to be treated as such..
//					myPrintR($v);

//					echo $key . " is a " . (is_a($v, "cmskvc") ? "cmskvc" :   gettype($v) ) . "<br/>";
					if (key_exists(EXTRACT_FILE_UPLOAD,$this->keys) && in_array($key,$this->keys[EXTRACT_FILE_UPLOAD])){
						if (is_array($v ) && key_exists('file',$v) && file_exists($v['file'])){
							$_FILES[$field] = $v;
							$_FILES[$field]['data'] =  array( file_get_contents($v['file']));
							$_FILES[$field]['tmp_name'] = null;
							$dataToWrite[$field] = true;
						}
					} else if (is_object($v) && is_a($v, "cmskvc")){//is_object()) {
						// if we are an object and we are a cmskvc.. get the documentcode from the cms kvc.. we have to assume that all documents have been saved.


						// we need to decide if it is an object and it has been saved.
						// if it has not been saved.. we are in the shit... as it has not got a document code.
						// we also then need to.. if we have an array.. we need.. to do what?
						//simple one to one join..

						//$documentCode =  $v->documentCode;
						if (!is_null( $v->documentCode ))
							$dataToWrite[$field] = $v->documentCode;
					//we need to remove the reliance on magic here
					} else if (is_array($v)) {
						// we are an array.. we assume that we either have a list of objects or documentcodes.
						//we need to go through the array.. and make a new one.. with just document codes..
						$nv = array();
						foreach ($v as $key=>$value){
							//either a number
							// or a cmskvc

							if (is_numeric($value)) {
								array_push($nv, $value);
							} else if (is_a($value, "cmskvc")) {
								$documentCode  = $value->documentCode;
								if (!is_null( $documentCode )){
									array_push($nv, $documentCode);
								} else  {}//ignore unsaved or squark
							} else {}//ignore.. or throw a hissey fit..
						}
						$dataToWrite[$field] = $nv;
					} else {
						//normal data
						$dataToWrite[$field] = $v;
					}
				}
			}

			$cmsWriter = new CmsWriter($this->formId, $this->documentCode, $dataToWrite, $authInfo, $db);
			$res = $cmsWriter->processFormData();
			$this->documentCode = $cmsWriter->documentCode;//get this back out possibly
			if ( $res->hasErrors){
				$this->takeValueForKey( $res, 'errorMessage');
			}
			//echo "save " . get_class($this) . "<br>\n";
			//myPrintR($res);
			if (!$res->hasErrors) {
				registerSessionPseudoSuperGlobal('_CMSKVC',$this->id, $this);
			}
			return $res;
		}

		// load could potentially be evil.. there is the potential to load a whole object graph		== lots of memory and loop hell
		// for this reason we need to be careful with the EXTRACT_TO_MANY_OBJECTS and EXTRACT_TO_ONE_OBJECT
		function load($id, &$authInfo, &$db){

			$this->log("attemping to load cmskvc object with id: " . $id);
			
			$cmsDocument = new CmsDocument($db);
			$cmsDocument->flush();
			$cmsDocument->addDocumentType($this->documentTypeCode);
			$cmsDocument->addFormId($this->formId);

			$cmsDocument->setFetchKeys(array_flip($this->dataModelKeyMapping ));
			$cmsDocument->addConstraint( $this->cmskvcidfield , $id);

			$cmsDocument->resetIterator();
			$object = $cmsDocument->nextDocument();

			$manyToManyTableWriter = null;//due to fucked up scoping and general bad behaviour.. we can declare these here..
			$contentTableWriter = null;

			if ($object){
				foreach ($object as $key => $value){
					//the only real problem will be serialising binary data.. ohwell.. worry about that later
					if (!is_null($value)) {
						if (  $this->canExtractKeyAs($key, EXTRACT_FILE_UPLOAD) ){
							// now we need to check.. for the document thing-a-ma-bob..
							// we do have problems with multiple scaled images, so  we will only fault/load the orignal
							$fileRecord = getBinaryRecordForDocumentCodeAndId( $this->documentCode,$value,$db );
							if (!is_null($fileRecord)){
								// it may be an advantage here.. to roll out .. the  image .. back to the disk cache.. not sure..
								$file = array();
								//$file['id'] = $fileRecord['id'];//is the id needed.. not really
								$fn =  CMS_KVC_UPLOAD_TEMP_IMAGES . "/" . $this->valueForKey('id') . "/" . $this->valueForKey('id') . "." . imageFileExtensionForMimeType( $fileRecord['mimetype']);
								$file['isSavedFile']  = true;
    							$file['name'] = $fileRecord['filename'];
    							$file['type'] = $fileRecord['mimetype'];
    							$file['tmp_name'] = null;
    							$file['file'] = $fn;
    							$file['documentCode'] = $fileRecord['documentcode'];
    							$file['elementId'] = $fileRecord['elementid'];
    							$file['error'] = 0;
    							$file['size'] = $fileRecord['filesize'];
    							$file['fileTypeCode'] = $fileRecord['filetypecode'];

    							$this->takeValueForKey($file, $key);
    							//since we are now uploaded
							}

						} else if ( $this->canExtractKeyAs($key, EXTRACT_TO_MANY_OBJECTS) ){

							// we take the value.. and read the correct related objects.
							// we always assume that in the array.. we have the objects id
							// as a key..
							if (is_null($manyToManyTableWriter)) {//fucking evil fucking scoping maddness..
								$manyToManyTableWriter = new CmsTableWriter("manytomanydata", $db);
							}
							$subQ = "WHERE elementid='" . $value . "' AND parentcode='" . $this->documentCode . "'";
							$manyToManyTableWriter->resetCustomIterator($subQ);
							$objectsForField = array();
							while($row = $manyToManyTableWriter->nextItem()) {
								//	echo $row->relatedcode;
								$object = $cmsDocument->openDocument( $row->relatedcode );
								if ($object) array_push($objectsForField, $object->id);
							}
							$this->takeValueForKey($objectsForField,$key);
						} else if ( $this->canExtractKeyAs($key, EXTRACT_TO_ONE_OBJECT) ) {
							//simply read the object with the document code
							// and fill in the gaps.
							//if (is_null($contentTableWriter)) {//fucking evil fucking scoping maddness..
							//	$contentTableWriter = new CmsTableWriter("content", $db);
							//}
							$object = $cmsDocument->openDocument($value);
							if ($object)
								$this->takeValueForKey($object->id, $key);
						} else {
							$this->takeValueForKey($value, $key);
						}
					}
				}
				//beacuse the object has loaded .. and changed its id.. we need to.. 
				// log.. for debug.
				$this->log("cmskvc load succsessful, fangling id: " . $this->id . "->" . $id);
				
				clearSessionPseudoSuperGlobal('_CMSKVC',$this->id);
				
				$this->id = $id;
				registerSessionPseudoSuperGlobal('_CMSKVC', $id, $this);
				
				return true;
			}
			$this->log("failed  to load cmskvc object with id: " .$id . " reason: " . print_r( $cmsDocument->db->lastError, true)  );
			return false;
		}

		function canExtractKeyAs($key,  $extractType ) {
			return (key_exists($extractType, $this->keys) &&
			is_array($this->keys[ $extractType ]) &&
			in_array($key ,$this->keys[ $extractType]));
		}
		
		function log($message){
			$file = &Log::singleton('file', CMS_SITE_DIR.'/cmsKvcLog/cmsKvcLog.txt', 'CmsKvcLog');
			$file->log($this->id." : ".$message."; IP address : ".$_SERVER['REMOTE_ADDR']."; User Agent : ".$_SERVER['HTTP_USER_AGENT']."; REQUEST URI : ".$_SERVER['REQUEST_URI']."\n");
		}		

	}